CTFSHOW卷王杯-pwn
根据官方 wp 学习了两道好题
# check in
# 思路
发现开了 **sandbox**
(之后再仔细分析),然后读入姓名那里有一个明显的格式化字符串漏洞,再之后可以读入 0x90
大小的数据,然而数组大小只有 0x80
,很明显是一个栈溢出,但是溢出的长度非常短,只有 0x10
,也就是只能覆盖 rbp
和 ret
,在程序的最后有 close(1)
关闭了标准输出的文件描述符,也就是我们无法泄露任何信息,包括最终得到的 flag
,最后再看下此题的保护:没有开 **Canary**
和 **PIE**
保护。
首先,格式化字符串的利用方式很显然,可以用于泄露 **libc**
:通过泄露 __libc_start_main + 243
,即可得到 libc_base
。
再来看栈溢出该如何利用,既然我们只能覆盖到 rbp
和 ret
,其中 ret
是跳转执行的地址,那么就可以考虑何处受 rbp
控制,又方便我们利用,不难想到 read
的时候,是将 0x90
的数据读到栈上的,而栈上的地址就受 rbp
控制,由汇编:
1 | 0x4013dd <main+163>: lea rax,[rbp-0x80] |
可见, read
的第二个参数 rsi
(写入数据的地址)就是 rbp-0x80
中的内容,因此,我们可以通过控制 **rbp**
为 **bss**
段上的某地址,然后再通过 **ret**
跳转到 **0x4013dd**
的位置,即可往 **bss**
段上写入内容,再之后通过一个栈迁移,即可跳转到我们读到 bss
段上的 gadget
并执行。
最后,我们来看一下这个 sandbox
,是个黑名单,禁用 socket
那些主要就是为了防止重启输出流造成非预期的,可以先不用管,主要就是发现禁用了 open
的系统调用和 read
相关的系统调用,虽然没有禁 write
相关的系统调用,但是由于有 close(1)
,所以也无法输出,这看似是无法 orw
了,不过仔细分析后可以发现: open
的系统调用虽然被禁用了,但是我们可以用 **openat**
系统调用来代替 **open**
系统调用( libc
中的 open
函数就是对 openat
这个底层系统调用的封装), openat
分绝对路径和相对路径两种写法, exp
中都给出了;再来看 read
,注意到 read
相关的系统调用并非全部被禁用了,当 read
的 fd
为 0
时, read
是可用的,对于常规 orw
来说,先 open
一个文件,由于 0,1,2
都分别被标准输入,输出,报错给占用了,所以文件描述符是从 3
开始的,而若是我们在 open
前,先 **close(0)**
,再 **open**
的话,我们打开的文件的描述符就是 **0**
了,我们也就可以 **read**
读取文件内容了;最后,对于 write
来说,可以采用 **“侧信道攻击” 的方式,就是对 flag
的每一位进行爆破,与我们已经 read
读入到内存中的真实 flag
进行比对,比如,若是相等就触发死循环,那么我们就可以通过判断接收数据用了多久来判断猜测是否正确了,在当前假设下,若是超过了 1
秒,则说明我们这一位爆破猜测成功了,当然,我这里写了一个 “二分法” 的版本,不然会耗费很长时间(其实, CTFshow
的 flag
好像用的是 uuid
字符串,也就是 {}
中的内容仅局限于 -0123456789abcdef
这几个字符,因此,应该还能进一步缩短我 exp
的爆破时长)。由于 “侧信道攻击” 最好使用 shellcode
来实现,故在之前需要用 mprotect
的 gadget
链改一下 bss
段的可执行权限,而一次性只能读入 0x80
大小的数据,可能无法将 orw
的 shellcode
和 mprotect
的 gadget
一起读进 bss
段,因此,我们可以先写一小段 ** **shellcode**
作为跳板和 mprotect
的 gadget
一起读入到 bss
段,再通过这个跳板,将 orw
的 shellcode
读到 bss
段上并跳转执行
# EXP
1 | from pwn import * |
# Incomplete Menu
# 思路
这题给出了一个不完整的菜单,只有 new
和 edit
, new
就是新建一个 ** 任意大小(无限制)** 的堆块,最多只可以创建 5
个堆块, edit
可以输入需要读进某堆块中内容的长度 len
,如果输入的长度 len
超过了该堆块的大小 size
,则实际读入长度 Len = size
,否则 Len = len
。漏洞点在于:在将读入内容的最后一字节改为 \x00
的时候,长度用的是用户输入的长度 len
,而并非实际读入的长度 Len
,这样就会导致某堆块后面的任意某字节会被 “刷零”,不过每个堆块只能被 edit
一次。
没有 show
,不能泄露信息,不过有走 IO
流输出的函数,如 puts
和 printf
,因此容易想到通过劫持 stdout
来进行信息泄露,没有 delete
函数,不能对堆块进行 free
,其实可以通过漏洞改 top chunk
的 size
,将它改小以后(要保证后三位不动),再申请一个大堆块,就能将原先的 top chunk
给 free
调了,不过在这里貌似并没有太大的用处。
我们只有这一个可利用的漏洞,又需要劫持到 stdout
,那就需要知道 stdout
与堆块地址的偏移,对于一般的堆块,其地址与 libc
地址的偏移肯定是无法确定的,但是这题可以申请任意大的堆块,也就是可以通过 mmap
申请堆块,而 **mmap**
申请出来的堆块,是紧接在 **libc**
的上方的,其地址与 **libc**
中地址的偏移是可以确定的,这里可以通过将 **_IO_2_1_stdout_**
的 **_IO_read_end**
和 **_IO_write_base**
的最后一字节都改为 **\x00**
,这样他们就相等了,也就可以通过走 IO
的输出函数泄露出其中( _IO_write_base ~ _IO_write_ptr
)包含的 libc
地址,进而得到 libc_base
。
泄露出 libc_base
之后,我们肯定是需要一个 “任意写” 漏洞,劫持一些函数或者 IO
流这些才能完成攻击。不难想到,可以通过劫持 stdin
来实现,这里我们按照和上面类似的方式,修改 **_IO_2_1_stdin_**
的 **_IO_buf_base**
中的最后一字节为 **\x00**
,这时, _IO_buf_base
正好指向了 _IO_2_1_stdin_
,而我们读入的时候,用的是 fgets
,这是一个走 IO
流的读入函数(这个函数就是读一整行到 stdin
缓冲区,然后再从缓冲区取出指定长度的数据,因此读数据会被 \n
截断,或者已经从缓冲区取到了所需长度的数据,也不再会刷新缓冲区往后读取数据了),因此,我们可以通过 fgets
读入任意内容到被伪造的 _IO_buf_base
( _IO_2_1_stdin_
)处,这样就可以再劫持一次 stdin
进行任意写了,我们读入多少字节到缓冲区, _IO_read_end
就会相应加多少,从缓冲区读取多少字节到目标内存, _IO_read_ptr
就会相应加多少,不过,最多也只能一次性读入 _IO_buf_end - _IO_buf_base
大小的数据到缓冲区,如果还需要读入,则会刷新缓冲区,一次也最多只能读取 _IO_read_end - _IO_read_ptr
大小的合法数据到目标内存,此时,由于 _IO_buf_end
为 _IO_buf_base + 132
,因此,我们只有读满 **132**
个字节,才有机会按我们第一次劫持 **stdin**
后,读入到 **_IO_buf_base**
中的值(记为 **_IO_buf_base(new)**
)刷新缓冲区,只有刷新完缓冲区之后,才能按照我们的设想进行第二次 **stdin**
的劫持。这里需要注意的是,在第一次完成 stdin
的劫持,读入 132
字节的内容到 _IO_2_1_stdin_
中之后,会尝试从缓冲区取 16
个字节到目标内存,如果成功取出了 16
个字节,也就满足了 fgets
的需要,那么也就不会刷新缓冲区了,我们也就不能对 stdin
进行第二次劫持了。在这里, glibc
是通过判断 **_IO_read_ptr**
是否小于 **_IO_read_end**
来判断缓冲区中是否还有剩余的数据,因此,我们可以在第一次劫持 stdin
往 _IO_2_1_stdin_
中写内容的时候,修改其中的 **_IO_read_ptr**
等于 **_IO_read_end**
,这里的 _IO_read_end
是指读完 132
个字节后的值( _IO_buf_base(new) + 132
),也就是需要 _IO_read_ptr = _IO_buf_base(new) + 132
,其实,这里也不一定是要加上 132
,略小一点,只要保证和 _IO_read_end
差值不足大约 16
个字节,可以有刷新缓冲区的机会即可,并且, glibc
源码中也只是判断了 _IO_read_ptr
是否小于 _IO_read_end
,故还可以将 _IO_read_ptr
改为大于 _IO_read_end
,比如 _IO_read_ptr = _IO_buf_base(new) + 200
也行。在这里,我是通过劫持 **IO_list_all**
来打 **FSOP**
的,通过读取 choice
的 fgets
进行 “任意写” 以后,由于获取到的值并非菜单中的选项 1
或 2
,就会走到 exit
,直接触发 FSOP
。
# EXP
1 | from pwn import * |
# 参考
1.BUUOJ PWN EXERCISE(二)
2.gyctf_2020_force
3.BUUCTF Pwn Exercise(一)
4.xdctf2015_pwn200
5.堆题总结
6.cmcc_simplerop
7.hitcontraining_unlink
8.0ctf2017-babyheap